iT邦幫忙

2023 iThome 鐵人賽

DAY 9
0
Software Development

Should I use fp-ts系列 第 9

[Should I use fp-ts?] Day 09 - fp-ts: Option flatten, flatMap(chain)

  • 分享至 

  • xImage
  •  

在本系列文中,所有的程式碼以及測試都可以在 should-i-use-fp-ts 找到,今日的範例放在 src/day-09 並且有習題和測試可以讓大家練習。

O.flatten

延續昨天的話題,昨天學會使用 O.map 讓普通的函式 (f: A => B) 可以維持在 Option 的規則下面運行。
但如果在 O.map 中使用有關判斷的函式 (f: A => Option<B>) ,會導致 Option 有嵌套的情況。
以下是昨天的例子,需求是做到第三步的時候將不及格的分數過濾出來視為 None,但 O.map 本身又會在函式回傳後再套上一層 Option

type IsFailed = (x: number) => O.Option<number>;
const isFailed: IsFailed = x => x > 60 ? O.some(x) : O.none;

const adjustScoreFailed: AdjustScore = flow(
  O.of, // Option<number>
  O.map(x => x * 1.2), // Option<number>
  O.map(isFailed), // Option<Option<number>>
  O.map(Math.round), // never: type mismatch
  O.map(x => x > 100 ? 100 : x), // never
);

這裡需要實作一個 flatten 函式,來將嵌套的 Option 整平。

/** fltten :: Option (Option a) -> Option a */
type Flatten = <A>(x: O.Option<O.Option<A>>) => O.Option<A>;
const flatten: Flatten = x => x._tag === 'None' ? O.none : x.value;

flatten 可以接受所有嵌套的 Option: Option<Option<A>>,並將嵌套減少一層,如此一來就可以使用在 adjustScore 之中。

const adjustScoreFlatten: AdjustScore = flow( // use 40 as an example
  O.of, // { _tag: 'Some', value: 40 }
  O.map(x => x * 1.2), // { _tag: 'Some', value: 48 }
  O.map(isFailed), // { _tag: 'None' }
  O.flatten, // { _tag: 'None' }
  O.map(Math.round), // { _tag: 'None' }
  O.map(x => x > 100 ? 100 : x), // { _tag: 'None' }
);

但如果有需要複數個判斷函式 (f: A => Option<B>),這樣寫就會造成充滿了 O.map, O.flatten 的組合,但 O.flatten 並沒有承載任何的業務邏輯,這樣會形成閱讀程式碼的斷點,可以觀看以下範例。

// example
pipe(
 xs, // [1, 2, 3, 4, 5]
 head, // { _tag: 'Some', value: 1 }
 O.map(inverse), // Option<{ _tag: 'Some', value: 1 }>
 O.flatten, // { _tag: 'Some', value: 1 }
 O.map(inverse), // Option<{ _tag: 'Some', value: 1 }>
 O.flatten, // { _tag: 'Some', value: 1 }
 O.map(inverse), // Option<{ _tag: 'Some', value: 1 }>
 O.flatten, // { _tag: 'Some', value: 1 }
)

O.flatMap(chain)

這邊需要再設計一個 flatMap 函式來將 O.map + O.flatten 的繁複操作組合起來,進一步簡化程式碼。

/** flatMap :: (a -> Option b) -> Option a -> Option b */
export type FlatMap = <A, B>(f: (a: A) => O.Option<B>) => 
						(x: O.Option<A>) => O.Option<B>;
export const flatMap: FlatMap = f => x => pipe(x, O.map(f), O.flatten);
// equals
export const flatMap: FlatMap = f => x => 
								x._tag === 'None' ? O.none : f(x.value);

O.flatMap 接收判斷函式 (f: A => Option<B>) 以及 (x: Option<A>),然後直接輸出經過 O.map, O.flatten 處理的函數。

const adjustScore: AdjustScore = flow( // use 40 as an example
  O.of, // { _tag: 'Some', value: 40 }
  O.map(x => x * 1.2), // { _tag: 'Some', value: 48 }
  O.flatMap(isFailed), // { _tag: 'None' }
  O.map(Math.round), // { _tag: 'None' }
  O.map(x => x > 100 ? 100 : x), // { _tag: 'None' }
);

今天的主題在 should-i-use-fp-ts src/day-09 有習題和測試可以練習,大家可以嘗試自己能不能根據 OptionO.map 的定義寫出自己的 flattenflatMap

Reference

Option.ts | fp-ts


上一篇
[Should I use fp-ts?] Day 08 - fp-ts: Option map
下一篇
[Should I use fp-ts?] Day 10 - fp-ts: Option getOrElse, getOrElseW
系列文
Should I use fp-ts25
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言